package com.firemoss.atonement.test.mapper
{
	import com.firemoss.atonement.mapper.Mapper;
	import com.firemoss.atonement.mapper.Mapping;
	import com.firemoss.atonement.mapper.PropertyResolver;
	import com.firemoss.atonement.mapper.error.InvalidMappingsError;
	import com.firemoss.atonement.mapper.resolution.SimpleAssignmentValueResolver;
	import com.firemoss.atonement.metadata.Property;
	import com.firemoss.atonement.test.mapper.objects.*;
	import com.firemoss.atonement.test.metadata.inspectors.property.objects.*;
	
	import flash.utils.Dictionary;
	
	import mx.collections.ArrayCollection;
	
	import org.flexunit.asserts.*;

	public class MapperTests
	{
		private var mapper : Mapper = new Mapper();
			
		[Test]
		public function mapper_supports_bulk_map_addition() : void
		{
			var mappings : Array = [
				new Mapping( SimpleObject, SimpleObjectDTO ),
				new Mapping( SimpleObject, DotNetStyleSimpleObjectDTO )
			]
				
			mapper.mappings = mappings
				
			assertTrue( mapper.mappingExistsBetween( SimpleObject, SimpleObjectDTO ) );
			assertTrue( mapper.mappingExistsBetween( SimpleObject, DotNetStyleSimpleObjectDTO ) );
		}
		
		[Test]
		public function mapper_supports_fluent_style_map_addition() : void
		{
			mapper.addMapping(
				new Mapping( SimpleObject, SimpleObjectDTO )
			).addMapping(
				new Mapping( SimpleObject, DotNetStyleSimpleObjectDTO )
			)
			
			assertTrue( mapper.mappingExistsBetween( SimpleObject, SimpleObjectDTO ) );
			assertTrue( mapper.mappingExistsBetween( SimpleObject, DotNetStyleSimpleObjectDTO ) );
			
		}
		
		[Test]
		public function map_table_can_be_reset() : void
		{
			mapper.addMapping(
				new Mapping( SimpleObject, SimpleObjectDTO )
			).addMapping(
				new Mapping( SimpleObject, DotNetStyleSimpleObjectDTO )
			)
			
			assertTrue( mapper.mappingExistsBetween( SimpleObject, SimpleObjectDTO ) );
			assertTrue( mapper.mappingExistsBetween( SimpleObject, DotNetStyleSimpleObjectDTO ) );
			
			mapper.resetMapTable()
				
			assertFalse( mapper.mappingExistsBetween( SimpleObject, SimpleObjectDTO ) );
			assertFalse( mapper.mappingExistsBetween( SimpleObject, DotNetStyleSimpleObjectDTO ) );
		}
		
		[Test]
		public function can_get_a_mapping() : void
		{
			var mapping : Mapping = new Mapping( SimpleObject, SimpleObjectDTO )

			mapper.addMapping( mapping )
			
			assertEquals( mapping, mapper.getMappingFor( SimpleObject, SimpleObjectDTO ) );
		}
		
		[Test]
		public function properties_are_resolved_when_map_is_added() : void
		{
			var mapping : Mapping = new Mapping( SimpleObject, SimpleObjectDTO );
			
			assertFalse( mapping.hasResolvedProperty( "getterProperty" ) );
				
			mapper.addMapping( mapping )
			
			assertTrue( mapping.hasResolvedProperty( "getterProperty" ) );
		}

		[Test]
		public function unresolved_properties_are_detected_when_map_is_added() : void
		{
			var mapping : Mapping = new Mapping( SimpleObject, DotNetStyleSimpleObjectDTO );
			
			assertFalse( mapping.hasResolvedProperty( "getterProperty" ) );
				
			mapper.addMapping( mapping )
			
			assertTrue( mapping.hasUnresolvedProperty( "GetterProperty" ) );
		}
		
		[Test]
		public function property_case_sensitivity_can_be_turned_off() : void
		{
			mapper = new Mapper()
			mapper.caseSensitivePropertyNames = false
				
			var mapping : Mapping = new Mapping( SimpleObject, DotNetStyleSimpleObjectDTO );
			
			mapper.addMapping( mapping )
			
			assertFalse( mapping.hasUnresolvedProperty( "GetterProperty" ) );
			
			var source : SimpleObject = new SimpleObject()
			source.getterProperty = "getterPropVal"
			var target : DotNetStyleSimpleObjectDTO = new DotNetStyleSimpleObjectDTO()
			
			assertNull( target.GetterProperty )
			
			mapper.map( source, target )
			
			assertEquals( source.getterProperty, target.GetterProperty )
		}
		
		[Test]
		public function a_custom_property_mapping_can_be_added() : void
		{
			var mapping : Mapping = new Mapping( SimpleObject, DotNetStyleSimpleObjectDTO );
			
			assertFalse( mapping.hasResolvedProperty( "GetterProperty" ) );
			
			mapper.addMapping( mapping )
			
			assertTrue( mapping.hasUnresolvedProperty( "GetterProperty" ) );
			
			mapping.addPropertyResolver( 
				new PropertyResolver(
					"getterProperty", 
					"GetterProperty", 
					SimpleAssignmentValueResolver.resolve 
				)
			);
			
			assertFalse( mapping.hasUnresolvedProperty( "GetterProperty" ) );
			assertTrue( mapping.hasResolvedProperty( "GetterProperty" ) );
		}
		
		[Test]
		public function custom_property_mappings_can_be_used_in_bulk_mapping_definition() : void
		{
			var badMapping : Mapping = new Mapping( SimpleObject, DotNetStyleSimpleObjectDTO )
				.addPropertyResolver( 
					new PropertyResolver(
						"getterProperty", 
						"GetterProperty", 
						SimpleAssignmentValueResolver.resolve 
					)
				);
			

			var mappings : Array = [ badMapping ]
				
			assertFalse( "GetterProperty was already resolved", badMapping.hasResolvedProperty( "GetterProperty" ) );
			
			mapper.mappings = mappings
			
			assertTrue( "PropertyProperty wasn't unresolved", badMapping.hasUnresolvedProperty( "PropertyProperty" ) );
			assertFalse( "GetterProperty was still unresolved", badMapping.hasUnresolvedProperty( "GetterProperty" ) );
		}
		
		[Test( expects="com.firemoss.atonement.mapper.error.InvalidMappingsError" )]
		public function assert_mappings_are_valid_finds_currently_unresolved_properties() : void
		{
			mapper.addMapping( new Mapping( Address, AddressDTO , false ) );
			
			mapper.assertMappingsAreValid()
		}
		
		
		[Test]
		public function target_properties_can_be_ignored_programatically() : void
		{
			mapper.addMapping( new Mapping( Contact, ContactDTO, false )
				.ignoreProperty( "ignoredProperty" )
				.ignoreProperty( "ignoredAggregation" )
				.setAggregatedType( "addressList", AddressDTO )
				.setAggregatedType( "addressArray", AddressDTO )
				.setAggregatedType( "addressDictionary", AddressDTO )
			)
			
			addValidAddressMapping( mapper )
			
			assertTrue( mapper.getMappingFor( Contact, ContactDTO ).assertMappingIsValid() );							
			
		}

		[Test]
		public function target_properties_can_be_ignored_declaratively() : void
		{
			var mapping : Mapping = new Mapping( Contact, ContactDTO, false );
			
			mapping.aggregatedPropertyTargetTypes = {
				"addressList" : AddressDTO,
				"addressArray" : AddressDTO,
				"addressDictionary" : AddressDTO
			}
				
			mapping.ignoredProperties = [ "ignoredProperty", "ignoredAggregation" ]
			
			mapper.mappings = [ mapping ]
			
			addValidAddressMapping( mapper )
			
			assertTrue( mapper.getMappingFor( Contact, ContactDTO ).assertMappingIsValid() );							
			
		}
		
		/**
		 * We should be able to define a mapping between two things where a map-based resolver is needed,
		 * define the required additional mapping _after_ the initial mapping, and still be considered valid.
		 */
		[Test]
		public function late_type_mapping_is_supported() : void
		{
			mapper.addMapping( new Mapping( Contact, ContactDTO )
				.ignoreProperty( "ignoredProperty" )
				.setAggregatedType( "addressList", AddressDTO )
				.setAggregatedType( "addressDictionary", AddressDTO )
			)

			addValidAddressMapping( mapper )
			
			assertTrue( mapper.getMappingFor( Contact, ContactDTO ).assertMappingIsValid() );							
			
		}
		
		[Test( expects="com.firemoss.atonement.mapper.error.InvalidMappingsError" )]
		public function mapping_asserts_validity_before_executing() : void
		{
			mapper.addMapping( new Mapping( Address, AddressDTO, false ) );
			
			mapper.map( new Address(), new AddressDTO() )
		}
		
		[Test]
		public function simple_mapping_works() : void
		{
			addValidAddressMapping( mapper )
			
			var addy : Address = createAddress()
				
			var dto : AddressDTO = new AddressDTO()
				
			mapper.map( addy, dto )
			
			assertTrue( dto.line1 == "line 1 value" );
			assertTrue( dto.line2 == "line 2 value" );
			assertTrue( dto.city == "city value" );
			assertTrue( dto.addressAsOneLine == "line 1 value line 2 value" )
			assertTrue( dto.state == "CODE" )
		}
		
		[Test]
		public function singular_aggregate_mapping_of_mapped_classes_works() : void
		{
			addValidContactMapping( mapper )
			
			var contact : Contact = createContact()
			var dto : ContactDTO = new ContactDTO()
			
			mapper.map( contact, dto )
				
			assertTrue( dto.firstName = "John" )
			assertTrue( dto.primaryAddress is AddressDTO )
			assertTrue( dto.primaryAddress.state == "CODE" );
		}
		
		[Test]
		public function list_to_array_mapping_works() : void
		{
			addValidContactMapping( mapper )
			
			var contact : Contact = createContact().set({
				addressList : new ArrayCollection( [
					createAddress().set({
						line1 : "nested1"
					}),
					createAddress().set({
						line1 : "nested2"
					})
				])
			})
			
			var dto : ContactDTO = new ContactDTO()
			
			mapper.map( contact, dto )
			
			assertTrue( dto.firstName = "John" )
			assertTrue( dto.primaryAddress is AddressDTO )
			assertTrue( dto.primaryAddress.state == "CODE" )
			assertNotNull( dto.addressList )
			assertTrue( dto.addressList is Array )
			assertEquals( 2, dto.addressList.length )
			assertTrue( dto.addressList[1].line1 = "nested2" )
		}
		
		[Test]
		public function array_to_list_mapping_works() : void
		{
			addValidContactMapping( mapper )
			
			var contact : Contact = createContact().set({
				addressArray :[
					createAddress().set({
						line1 : "nested1"
					}),
					createAddress().set({
						line1 : "nested2"
					})
				]
			})
			
			var dto : ContactDTO = new ContactDTO()
			
			mapper.map( contact, dto )
			
			assertTrue( dto.firstName = "John" )
			assertTrue( dto.primaryAddress is AddressDTO )
			assertTrue( dto.primaryAddress.state == "CODE" )
			assertNotNull( dto.addressArray )
			assertTrue( dto.addressArray is ArrayCollection )
			assertEquals( 2, dto.addressArray.length )
			assertTrue( dto.addressArray[1].line1 = "nested2" )
		}
		
		[Test]
		public function explicit_list_mapping_works() : void
		{
			addValidAddressMapping( mapper ).addMapping( new Mapping( Contact, ContactDTO )
				.ignoreProperty( "ignoredProperty" )
				.setAggregatedType( "addressDictionary", AddressDTO )
				.addPropertyResolver( 
					new PropertyResolver(
						"addressList", 
						"addressList", 
						function( source : *, targetProperty : Property, mapping : Mapping, mappingChain : Dictionary ) : Array {
							return [ 1, 2 ];
						} 
					)
				)
			)
			
			var contact : Contact = createContact().set({
				addressList : new ArrayCollection( [ "one", "two" ])
			})
			
			var dto : ContactDTO = new ContactDTO()
			
			mapper.map( contact, dto )
			
			assertTrue( dto.firstName = "John" )
			assertTrue( dto.primaryAddress is AddressDTO )
			assertTrue( dto.primaryAddress.state == "CODE" )
			assertNotNull( dto.addressList )
			assertEquals( 2, dto.addressList.length )
			assertTrue( dto.addressList[1] = 2 )
		}

		
		[Test]
		public function dictionary_mapping_works() : void
		{
			addValidContactMapping( mapper )
			
			var dict : Dictionary = new Dictionary()
			dict[ "addy1" ] = createAddress().set({
				line1 : "nestedDict1"
			})
			dict[ "addy2" ] = createAddress().set({
				line1 : "nestedDict2"
			})

			var contact : Contact = createContact().set({
				addressDictionary : dict
			})
			
			var dto : ContactDTO = new ContactDTO()
			
			mapper.map( contact, dto )
			
			assertTrue( dto.firstName = "John" )
			assertTrue( dto.primaryAddress is AddressDTO )
			assertTrue( dto.primaryAddress.state == "CODE" )
			assertNotNull( dto.addressDictionary )
			assertTrue( dto.addressDictionary[ "addy2" ].line1 = "nestedDict2" )
		}
		
		[Test]
		public function maps_can_be_implicitly_created() : void
		{
			var contact : Contact = createContact().set({
				addressList : new ArrayCollection( [
					createAddress().set({
						line1 : "nested1"
					}),
					createAddress().set({
						line1 : "nested2"
					})
				])
			})
			
			var dto : ContactDTO = new ContactDTO()
			
			mapper.map( contact, dto )
			
			assertTrue( dto.firstName = "John" )
			assertTrue( dto.primaryAddress is AddressDTO )
			assertTrue( dto.primaryAddress.state == "CODE" )
			assertNotNull( dto.addressList )
			assertEquals( 2, dto.addressList.length )
			assertTrue( dto.addressList[1].line1 = "nested2" )
		}
		
		[Test]
		public function declarative_mapping_configuration_works_fully() : void
		{
			var mapper : Mapper = new MappingDefinitions().mapper
				
			mapper.assertMappingsAreValid()
				
			fullMappingAssertions( mapper )
		}
		
		[Test]
		public function implicit_mapping_configuration_works_fully() : void
		{
			var mapper : Mapper = new Mapper()
			fullMappingAssertions( mapper )
		}
		
		[Test]
		public function programmatic_mapping_configuration_works_fully() : void
		{
			var mapper : Mapper = new Mapper()
			
			mapper = new Mapper()
				.addMapping(
					new Mapping( Contact, ContactDTO, false )
						.setAggregatedType( "addressList", AddressDTO )
						.setAggregatedType( "addressDictionary", AddressDTO )
						.setAggregatedType( "addressArray", AddressDTO )
						.ignoreProperty( "ignoredProperty" )
						.ignoreProperty( "ignoredAggregation" )
					)
				.addMapping(
					new Mapping( Address, AddressDTO, false )
						.addPropertyResolver(
							new PropertyResolver( "state", "state", new AddressDTO().stateCode )
						)
						.addPropertyResolver(
							new PropertyResolver( null, "addressAsOneLine", new AddressDTO().oneLineAddress )
						)
				)

			mapper.assertMappingsAreValid()
				
			fullMappingAssertions( mapper )
		}

		[Test]
		public function collection_properties_honor_programmtic_ignored_property_before_invalidating_due_to_unknown_aggregated_type() : void
		{
			var mapper : Mapper = new Mapper()
			
			mapper = new Mapper()
				.addMapping(
					new Mapping( Contact, ContactDTO, false )
					.setAggregatedType( "addressList", AddressDTO )
					.setAggregatedType( "addressDictionary", AddressDTO )
					.setAggregatedType( "addressArray", AddressDTO )
					.ignoreProperty( "ignoredProperty" )
					.ignoreProperty( "ignoredAggregation" )
				)
				.addMapping(
					new Mapping( Address, AddressDTO, false )
					.addPropertyResolver(
						new PropertyResolver( "state", "state", new AddressDTO().stateCode )
					)
					.addPropertyResolver(
						new PropertyResolver( null, "addressAsOneLine", new AddressDTO().oneLineAddress )
					)
				)
			
			mapper.assertMappingsAreValid()
		}
		
		[Test]
		public function collections_of_simple_types_can_be_mapped() : void
		{
			var mapper : Mapper = new Mapper()
			var contact : Contact = createContact()
				
			contact.stringArray = ["One","Two"]
			
			var contactDTO : ContactDTO = mapper.map( contact, new ContactDTO() ) as ContactDTO
			
			assertTrue( contactDTO.stringArray != null )
			assertTrue( contactDTO.stringArray.length == 2 )
			assertTrue( contactDTO.stringArray[1] == "Two" );
		}

		
		// HELPERS
		
		private function fullMappingContact() : Contact
		{
			var dict : Dictionary = new Dictionary()
			dict[ "addy1" ] = createAddress().set({
				line1 : "nestedDict1"
			})
			dict[ "addy2" ] = createAddress().set({
				line1 : "nestedDict2"
			})
			
			var addressList : ArrayCollection = new ArrayCollection( [
				createAddress().set({
					line1 : "nested1"
				}),
				createAddress().set({
					line1 : "nested2"
				})
			])
			
			return createContact().set({
				addressDictionary : dict,
				addressList : addressList
			})
		}
		
		private function fullMappingAssertions( mapper : Mapper ) : void
		{
			var contact : Contact = fullMappingContact()
			var dto : ContactDTO = new ContactDTO()
			
			mapper.map( contact, dto )
			
			assertTrue( dto.firstName = "John" )
			assertTrue( dto.primaryAddress is AddressDTO )
			assertTrue( dto.primaryAddress.state == "CODE" )
			assertNotNull( dto.addressList )
			assertEquals( 2, dto.addressList.length )
			assertTrue( dto.addressList[1].line1 = "nested2" )
			assertNotNull( dto.addressDictionary )
			assertTrue( dto.addressDictionary[ "addy2" ].line1 = "nestedDict2" )
		}
		
		private function addValidAddressMapping( mapper : Mapper ) : Mapper
		{
			return mapper.addMapping( new Mapping( Address, AddressDTO ) )
		}
		
		private function addValidContactMapping( mapper : Mapper ) : Mapper
		{
			return addValidAddressMapping( mapper ).addMapping( new Mapping( Contact, ContactDTO ) )
  		}
		
		private function createAddress() : Address
		{
			return new Address({
				line1 : "line 1 value",
				line2 : "line 2 value",
				city : "city value",
				state : new State( { code : "CODE", name : "Name" } ),
				zip : "zip value"
			})
		}
		
		private function createContact() : Contact
		{
			return new Contact({
				firstName : "John",
				lastName : "Doe",
				primaryAddress : createAddress()
			})
		}
					
	}
}